<template>
  <transition :name="transitionName">
    <keep-alive :include="include">
      <component :is="component" />
    </keep-alive>
  </transition>
</template>

<style lang="scss">
#app {
  position: relative;
  & > * {
    position: absolute;
    width: 100vw;
    min-height: 100vh;
    background-color: #ffffff;
    top: 0;
    left: 0;
  }
}

.slide-left-enter-active,
.slide-left-leave-active {
  transition: all 250ms ease;
}

.slide-left-enter-from {
  transform: translateX(100%);
  opacity: 0;
}

.slide-left-enter-to,
.slide-left-leave-from {
  transform: translateX(0);
  opacity: 1;
}

.slide-left-leave-to {
  transform: translateX(-100%);
  opacity: 0;
}

.slide-right-enter-active,
.slide-right-leave-active {
  transition: all 250ms ease;
}

.slide-right-enter-from {
  transform: translateX(-100%);
  opacity: 0;
}

.slide-right-enter-to,
.slide-right-leave-from {
  transform: translateX(0);
  opacity: 1;
}

.slide-right-leave-to {
  transform: translateX(100%);
  opacity: 0;
}
</style>

<script lang="ts">
import {
  VNode,
  RendererNode,
  RendererElement,
  App,
  reactive,
  computed,
  markRaw,
  provide,
} from "vue";
import { Router, RouteLocationNormalized } from "vue-router";

export default {
  name: "PageStack",
};

// 路由方向
export enum Direction {
  Forward = "Forward",
  Back = "Back",
  Replace = "Replace",
  Unknow = "Unknow",
}

// 路由组件
type RouteComponent = VNode<
  RendererNode,
  RendererElement,
  { [key: string]: any }
>;

// use 参数
export type PageStackOptions = {
  router: Router;
  stackKey: string;
};

// 缓存的信息
const StackInfo = reactive({
  // 方向
  direction: Direction.Unknow,
  // 过度动画
  transitionName: "",
  // 当前组件
  component: null as RouteComponent,
  // 组件列表 路由栈
  components: [] as RouteComponent[],
  // 后退或前进多少个页面
  delta: 0,
  // 是否调用了 go 方法
  isUseGo: false,
});

// 判断地址是否包含 key
function hasKey(query, keyName) {
  return !!query[keyName];
}

// 获取 key
function getKey(src) {
  return src.replace(/[xy]/g, function (c) {
    let r = (Math.random() * 16) | 0;
    let v = c === "x" ? r : (r & 0x3) | 0x8;
    return v.toString(16);
  });
}

export function install(app: App, options: PageStackOptions) {
  if (!options.router) {
    throw new Error("PageStack options router are necessary.");
  }

  app.use(options.router);

  const keyName = options.stackKey || "stack-key";

  // 获取路由组件
  async function getRouteComponent(to: RouteLocationNormalized) {
    const route = options.router.options.routes.find(
      (item) => item.name === to.name
    ) as any;

    let routeComponent: any = null;

    if (typeof route.component === "function") {
      // 使用 defineAsyncComponent 会无法用 keep-alive 根据 name 缓存，暂时不知道怎么解决
      // routeComponent = defineAsyncComponent(route.component as any);
      const res = await (route.component as any)();
      routeComponent = Object.assign({}, res.default);
    } else {
      routeComponent = Object.assign({}, route.component);
    }

    // 把组件的 name 设置成 key
    routeComponent.name = to.query[keyName];

    return markRaw(routeComponent);
  }

  options.router.beforeEach(async function (to, from, next) {
    if (!hasKey(to.query, keyName)) {
      to.query[keyName] = getKey("xxxxxxxx");
      return next({ ...to });
    }

    let _component: RouteComponent = null;

    if (StackInfo.direction === Direction.Unknow) {
      // 初始化时
      _component = await getRouteComponent(to);
      StackInfo.components.push(_component);
    } else if (StackInfo.direction === Direction.Forward) {
      // 前进
      _component = await getRouteComponent(to);
      StackInfo.components.push(_component);
      StackInfo.transitionName = "slide-left";
    } else if (StackInfo.direction === Direction.Replace) {
      // 替换
      _component = await getRouteComponent(to);
      StackInfo.components.pop();
      StackInfo.components.push(_component);
      StackInfo.transitionName = "";
    } else if (StackInfo.direction === Direction.Back) {
      // 回退
      // StackInfo.components.pop();
      StackInfo.components.splice(
        StackInfo.components.length - Math.abs(StackInfo.delta)
      );
      if (StackInfo.components.length) {
        // 路由栈还有路由
        _component = StackInfo.components[StackInfo.components.length - 1];
      } else {
        // 路由栈没有路由，根据 route 再生成
        _component = await getRouteComponent(to);
        StackInfo.components.push(_component);
      }
      StackInfo.transitionName = "slide-right";
    }
    StackInfo.component = _component;

    // 设置页面标题
    if (to.meta.title) {
      document.title = to.meta.title;
    }
    next();
  });

  // 前进
  const push = options.router.push;
  options.router.push = function (...args: Parameters<typeof push>) {
    StackInfo.direction = Direction.Forward;
    return push(...args);
  };

  // 替换
  const replace = options.router.replace;
  options.router.replace = function (...args: Parameters<typeof replace>) {
    StackInfo.direction = Direction.Replace;
    return replace(...args);
  };

  const go = options.router.go;
  options.router.go = function (delta: number) {
    // 使用 go 方法必定会触发下面监听
    StackInfo.isUseGo = true;
    if (delta < 0) {
      // 回退
      StackInfo.direction = Direction.Back;
      StackInfo.delta = delta;
    } else {
      // 前进
      StackInfo.direction = Direction.Forward;
      // 暂时没想到办法解决 delta 大于 1 的情况
      delta = 1;
    }
    return go(delta);
  };

  options.router.options.history.listen(async (to, from, information) => {
    if (information.direction === "back") {
      // 回退
      StackInfo.direction = Direction.Back;
      // 如果使用 go 方法就根据 delta 返回，其它情况默认 -1
      StackInfo.delta = StackInfo.isUseGo ? information.delta : -1;
    } else if (information.direction === "forward") {
      // 前进
      StackInfo.direction = Direction.Forward;
      // 暂时没想到办法解决 delta 大于 1 的情况
      StackInfo.delta = 1;
    }
    // 将是否使用 go 重设
    StackInfo.isUseGo = false;
  });
}

// 提供注入
export const ProvideKey = "PageStackProvideKey";

export const provideValue = {
  getStackInfo: () => StackInfo,
};

export type ProvideValue = typeof provideValue;
</script>

<script setup lang="ts">
const transitionName = computed(() => StackInfo.transitionName);

const component = computed(() => StackInfo.component);

const include = computed(() =>
  StackInfo.components.map((item: any) => item.name)
);

// 提供注入
provide(ProvideKey, provideValue);
</script>
